<!DOCTYPE html>
<html lang="zh-CN">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="robots" content="noodp" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
        <title>《Flutter之precacheImage（）解析》 - 飞雪无情的博客</title><meta name="Description" content="专注于IT互联网，包括但不限于Go语言(golang)、Java、Android、Python、项目管理、抖音分析、软件架构等"><meta property="og:title" content="《Flutter之precacheImage（）解析》" />
<meta property="og:description" content="
本文主要介绍Flutter 中 precacheImage() 方法是如何提高图片加载速度的？
" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://www.flysnow.org/posts/flutter/Flutter%E4%B9%8BprecacheImage%E8%A7%A3%E6%9E%90/" />
<meta property="article:published_time" content="2021-07-31T21:12:42+08:00" />
<meta property="article:modified_time" content="2021-07-31T21:12:42+08:00" />
<meta name="twitter:card" content="summary"/>
<meta name="twitter:title" content="《Flutter之precacheImage（）解析》"/>
<meta name="twitter:description" content="
本文主要介绍Flutter 中 precacheImage() 方法是如何提高图片加载速度的？
"/>
<meta name="application-name" content="飞雪无情的博客">
<meta name="apple-mobile-web-app-title" content="飞雪无情的博客"><link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
        <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
        <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"><link rel="manifest" href="/site.webmanifest"><link rel="canonical" href="https://www.flysnow.org/posts/flutter/Flutter%E4%B9%8BprecacheImage%E8%A7%A3%E6%9E%90/" /><link rel="prev" href="https://www.flysnow.org/posts/flutter/Flutter-drawFrame/" /><link rel="next" href="https://www.flysnow.org/posts/flutter/Flutter%E4%B9%8BStack%E7%94%A8%E6%B3%95%E5%B0%8F%E7%BB%93/" /><link rel="stylesheet" href="/lib/normalize/normalize.min.css"><link rel="stylesheet" href="/css/style.min.css"><link rel="stylesheet" href="/lib/fontawesome-free/all.min.css"><link rel="stylesheet" href="/lib/animate/animate.min.css"><script type="application/ld+json">
    {
        "@context": "http://schema.org",
        "@type": "BlogPosting",
        "headline": "《Flutter之precacheImage（）解析》",
        "inLanguage": "zh-CN",
        "mainEntityOfPage": {
            "@type": "WebPage",
            "@id": "https:\/\/www.flysnow.org\/posts\/flutter\/Flutter%E4%B9%8BprecacheImage%E8%A7%A3%E6%9E%90\/"
        },"genre": "posts","keywords": "flutter","wordcount":  1422 ,
        "url": "https:\/\/www.flysnow.org\/posts\/flutter\/Flutter%E4%B9%8BprecacheImage%E8%A7%A3%E6%9E%90\/","datePublished": "2021-07-31T21:12:42+08:00","dateModified": "2021-07-31T21:12:42+08:00","publisher": {
            "@type": "Organization",
            "name": "飞雪无情"},"author": {
                "@type": "Person",
                "name": "飞雪无情"
            },"description": ""
    }
    </script></head>
    <body header-desktop="" header-mobile=""><script type="text/javascript">(window.localStorage && localStorage.getItem('theme') ? localStorage.getItem('theme') === 'dark' : ('' === 'auto' ? window.matchMedia('(prefers-color-scheme: dark)').matches : '' === 'dark')) && document.body.setAttribute('theme', 'dark');</script>

        <div id="mask"></div><div class="wrapper"><div class="logo-wrapper">
  <a href="/%20/" class="logo">飞雪无情的博客</a>
</div>

<nav class="site-navbar">
  <ul id="menu" class="menu">
    <li class="menu-item">
      <a class="menu-item-link" href="/tools/">工具</a>
    </li><li class="menu-item">
      <a class="menu-item-link" href="/archives/">归档</a>
    </li><li class="menu-item">
      <a class="menu-item-link" href="/about/">关于</a>
    </li>
  </ul>
</nav><main class="main">
                <div class="container"><div class="toc" id="toc-auto">
            <h2 class="toc-title">Contents</h2>
            <div class="toc-content" id="toc-content-auto"></div>
        </div><article class="page single"><h1 class="single-title animated flipInX">《Flutter之precacheImage（）解析》</h1><div class="post-meta">
            <div class="post-meta-line"><span class="post-author"><a href="/" title="Author" rel=" author" class="author"><i class="fas fa-user-circle fa-fw"></i>飞雪无情</a></span>&nbsp;<span class="post-category">included in <a href="/categories/Flutter/"><i class="far fa-folder fa-fw"></i>Flutter</a></span></div>
            <div class="post-meta-line"><i class="far fa-calendar-alt fa-fw"></i>&nbsp;<time datetime="2021-07-31">2021-07-31</time>&nbsp;<i class="fas fa-pencil-alt fa-fw"></i>&nbsp;1422 words&nbsp;
                <i class="far fa-clock fa-fw"></i>&nbsp;3 minutes&nbsp;</div>
        </div><div class="featured-image"><img
        class="lazyload"
        src="/svg/loading.min.svg"
        data-src="https://luckly007.oss-cn-beijing.aliyuncs.com/img/90c6cc12-742e-4c9f-b318-b912f163b8d0.png"
        data-srcset="https://luckly007.oss-cn-beijing.aliyuncs.com/img/90c6cc12-742e-4c9f-b318-b912f163b8d0.png, https://luckly007.oss-cn-beijing.aliyuncs.com/img/90c6cc12-742e-4c9f-b318-b912f163b8d0.png 1.5x, https://luckly007.oss-cn-beijing.aliyuncs.com/img/90c6cc12-742e-4c9f-b318-b912f163b8d0.png 2x"
        data-sizes="auto"
        alt="https://luckly007.oss-cn-beijing.aliyuncs.com/img/90c6cc12-742e-4c9f-b318-b912f163b8d0.png"
        title="https://luckly007.oss-cn-beijing.aliyuncs.com/img/90c6cc12-742e-4c9f-b318-b912f163b8d0.png" /></div><div class="details toc" id="toc-static"  kept="">
                <div class="details-summary toc-title">
                    <span>Contents</span>
                    <span><i class="details-icon fas fa-angle-right"></i></span>
                </div>
                <div class="details-content toc-content" id="toc-content-static"><nav id="TableOfContents">
  <ul>
    <li><a href="#imageproviderresolve"><code>ImageProvider.resolve()</code></a></li>
    <li><a href="#imagecacheputifabsent"><code>ImageCache.putIfAbsent()</code></a></li>
    <li><a href="#imageproviderload"><code>ImageProvider.load()</code></a></li>
  </ul>
</nav></div>
            </div><div class="content" id="content"><blockquote>
<p>本文主要介绍Flutter 中 <code>precacheImage()</code> 方法是如何提高图片加载速度的？</p>
</blockquote>
<p>Flutter 文档中提到调用 <a href="https://api.flutter.dev/flutter/widgets/precacheImage.html" target="_blank" rel="noopener noreffer">precacheImage()</a> 预先加载图片加载到缓存，后续如果这张图片被 <code>Image</code> 控件用到的话，加载起来会更快。</p>
<blockquote>
<p>Prefetches an image into the image cache.</p>
<p>If the image is later used by an [Image] or [BoxDecoration] or [FadeInImage], it will probably be loaded faster.</p>
</blockquote>
<p><code>precacheImage()</code> 代码如下：</p>
<pre><code>Future&lt;void&gt; precacheImage(
  ImageProvider provider,
  BuildContext context, {
  Size size,
  ImageErrorListener onError,
}) {
  final ImageConfiguration config = ...;
  final Completer&lt;void&gt; completer = Completer&lt;void&gt;();
  final ImageStream stream = provider.resolve(config);
  ...
  return completer.future;
}
</code></pre><p><code>precacheImage()</code> 是如何加快图片加载速度的？我们可以从如下两个角度来考虑这个问题：</p>
<ul>
<li>写缓存 - 如何将图片加入缓存</li>
<li>读缓存 - 如何从缓存取出图片</li>
</ul>
<h1 id="写缓存">写缓存</h1>
<p>先上结论：<strong><code>precacheImage()</code> 调用 <code>ImageProvider.resolve()</code> 从缓存中取出图片</strong>。</p>
<p><code>precacheImage()</code> 主要的调用关系如下：</p>
<pre><code>precacheImage() -&gt;
 ImageProvider.resolve() -&gt;
  ImageCache.putIfAbsent() -&gt;
   ImageProvider.load() -&gt;
</code></pre><h2 id="imageproviderresolve"><code>ImageProvider.resolve()</code></h2>
<pre><code> ImageStream resolve(ImageConfiguration configuration) {
    ...;
    final ImageStream stream = ImageStream();
    T obtainedKey;

    key = obtainKey(configuration);
    key.then&lt;void&gt;((T key) {
      obtainedKey = key;
      final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
        key,
        () =&gt; load(key, PaintingBinding.instance.instantiateImageCodec),
        onError: handleError,
      );
      if (completer != null) {
        stream.setCompleter(completer);
      }
    }).catchError(handleError);
    return stream;
}
</code></pre><p>注意 <code>imageCache.putIfAbsent()</code> 第二个参数形式如下：</p>
<pre><code>ImageStreamCompleter putIfAbsent(
  Object key,
  ImageStreamCompleter loader(), 
  { ImageErrorListener onError })
</code></pre><p>所以这里的 <code>loader</code> 参数实际上是对 <code>load()</code> 方法的封装。</p>
<h2 id="imagecacheputifabsent"><code>ImageCache.putIfAbsent()</code></h2>
<p><code>ImageCache</code> 用于缓存图片的类。这个类实现了 LRU 算法，最多可以保存1000个图片或者100MB。</p>
<pre><code>class ImageCache {
  final Map&lt;Object, _PendingImage&gt; _pendingImages = &lt;Object, _PendingImage&gt;{};
  final Map&lt;Object, _CachedImage&gt; _cache = &lt;Object, _CachedImage&gt;{};
</code></pre><ul>
<li><code>_pendingImages</code> - 保存正在加载中的图片</li>
<li><code>_cache</code> - 保存已加载到缓存的图片</li>
</ul>
<p><code>putIfAbsent()</code> 方法是 <code>ImageCache</code> 主要的入口方法，代码如下：</p>
<pre><code>ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) {
  ImageStreamCompleter result = _pendingImages[key]?.completer;
  // Nothing needs to be done because the image hasn't loaded yet.
  if (result != null)
    return result;
  // Remove the provider from the list so that we can move it to the
  // recently used position below.
  final _CachedImage image = _cache.remove(key);
  if (image != null) {
    _cache[key] = image;
    return image.completer;
  }
  try {
    result = loader();
  } catch (error, stackTrace) {
    ...
  }
  void listener(ImageInfo info, bool syncCall) {
    // Images that fail to load don't contribute to cache size.
    final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
    final _CachedImage image = _CachedImage(result, imageSize);
    ...
    final _PendingImage pendingImage = _pendingImages.remove(key);
    if (pendingImage != null) {
      pendingImage.removeListener();
    }

    _cache[key] = image;
    ...
  }
  if (maximumSize &gt; 0 &amp;&amp; maximumSizeBytes &gt; 0) {
    final ImageStreamListener streamListener = ImageStreamListener(listener);
    _pendingImages[key] = _PendingImage(result, streamListener);
    // Listener is removed in [_PendingImage.removeListener].
    result.addListener(streamListener);
  }
  return result;
}
</code></pre><p>其过程总结如下：</p>
<ul>
<li>
<p>如果 <code>_pendingImages</code> 存在对应的 key，说明正在加载该图片，此时什么也不用做所以直接返回</p>
</li>
<li>
<p>如果 <code>_cache</code> 存在对应的 key，说明已经加载过该图片，此时将图片调整到 LRU 位置并返回</p>
</li>
<li>
<p>否则，调用</p>
<pre><code>loader()
</code></pre><p>来加载图片。</p>
<pre><code>loader()
</code></pre><p>的返回值为</p>
<pre><code>result
</code></pre><ul>
<li>为 <code>result</code> 创建新对象并保存到 <code>_pendingImages</code> 中</li>
<li>监听图片加载过程。加载成功后从 <code>_pendingImages</code> 移除对应的 key，并在 <code>_cache</code> 中保存加载结果</li>
</ul>
</li>
</ul>
<h2 id="imageproviderload"><code>ImageProvider.load()</code></h2>
<p><code>ImageProvider</code> 是一个抽象类，它的 <code>load()</code> 和 <code>obtainKey()</code> 是抽象方法，需要 <code>ImageProvider</code> 的子类实现。</p>
<p>以 <code>AssetBundleImageProvider</code> 为例，<code>load()</code> 方法如下：</p>
<pre><code>abstract class AssetBundleImageProvider extends ImageProvider&lt;AssetBundleImageKey&gt; {
  @override
  ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, decode),
      scale: key.scale,
      informationCollector: () sync* {
        yield DiagnosticsProperty&lt;ImageProvider&gt;('Image provider', this);
        yield DiagnosticsProperty&lt;AssetBundleImageKey&gt;('Image key', key);
      },
    );
  }
  
  @protected
  Future&lt;ui.Codec&gt; _loadAsync(AssetBundleImageKey key, DecoderCallback decode) async {
    final ByteData data = await key.bundle.load(key.name);
    if (data == null)
      throw 'Unable to read data';
    return await decode(data.buffer.asUint8List());
  }
}
</code></pre><p>从代码不难看出这里的 <code>_loadAsync()</code> 即底层的 asset 图片加载过程，它由 <code>load()</code> 方法调用。而 <code>load()</code> 是前面 <code>result = loader();</code> 的具体执行过程。</p>
<p>至此可知，调用 <code>precache()</code> 可将指定 key 对应的 <code>_CachedImage</code> 保存到 <code>ImageCache</code> 中。</p>
<h1 id="读缓存">读缓存</h1>
<p>先上结论：<strong><code>Image</code> 调用 <code>ImageProvider.resolve()</code> 从缓存中取出图片</strong>。</p>
<p><code>Image</code> 控件用于显示图片。有多种方式显示图片：</p>
<ul>
<li><code>Image.asset</code> - 使用 key 从 <code>AssetBundle</code> 图片</li>
<li><code>Image.network()</code> - 使用 url 从网络加载图片</li>
<li><code>Image.file</code> - 从文件加载图片</li>
<li><code>Image.memory</code> - 从内存加载图片</li>
</ul>
<p>这些显示方式都有各自的 <code>ImageProvider</code>：</p>
<ul>
<li><code>AssetImage</code></li>
<li><code>NetworkImage</code></li>
<li><code>FileImage</code></li>
<li><code>MemoryImage</code></li>
</ul>
<p>上述这些 Image 都继承自 <code>ImageProvider</code>。</p>
<pre><code>abstract class ImageProvider&lt;T&gt; {
  ImageStream resolve(ImageConfiguration configuration) {}
  Future&lt;bool&gt; evict({ ImageCache cache, ImageConfiguration configuration = ImageConfiguration.empty }) async {}
  Future&lt;T&gt; obtainKey(ImageConfiguration configuration);
  ImageStreamCompleter load(T key, DecoderCallback decode);
}

abstract class AssetBundleImageProvider extends ImageProvider&lt;AssetBundleImageKey&gt; {}

abstract class NetworkImage extends ImageProvider&lt;NetworkImage&gt; {

class FileImage extends ImageProvider&lt;FileImage&gt; {

class MemoryImage extends ImageProvider&lt;MemoryImage&gt; {}
</code></pre><p><code>Image</code> 它是一个 <code>StatefulWidget</code>，它依赖于其 <code>ImageProvider</code> 成员，主要代码如下：</p>
<pre><code>class Image extends StatefulWidget {
 final ImageProvider image;
  _ImageState createState() =&gt; _ImageState();
}

class _ImageState extends State&lt;Image&gt; with WidgetsBindingObserver {
  @override
  void didChangeDependencies() {
    ...
    _resolveImage();
    ...
    super.didChangeDependencies();
  }

  @override
  void didUpdateWidget(Image oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.image != oldWidget.image)
      _resolveImage();    
  }
  
  void _resolveImage() {
    final ImageStream newStream =
      widget.image.resolve(createLocalImageConfiguration(
        context,
        size: widget.width != null &amp;&amp; widget.height != null ? Size(widget.width, widget.height) : null,
      ));
    _updateSourceStream(newStream);
  }
}
</code></pre><p>注意这里的 <code>_resolveImage()</code> 方法，它调用 <code>ImageProvider.resolve()</code>。所以可以认为是由 <code>ImageProvider.resolve()</code> 从缓存中取出图片(前面分析过 <code>ImageProvider.resolve()</code> 与图片缓存的关系)。</p>
<h1 id="总结">总结</h1>
<p><code>precacheImage()</code> 提升图片加载速度的关键其实在于主动调用 <code>ImageProvider.resolve()</code> 来提前将图片加载到 <code>ImageCache</code>，示意图如下(这里假设从本地读取图片)：</p>
<p><img
        class="lazyload"
        src="/svg/loading.min.svg"
        data-src="https://luckly007.oss-cn-beijing.aliyuncs.com/img/15814253180199.jpg"
        data-srcset="https://luckly007.oss-cn-beijing.aliyuncs.com/img/15814253180199.jpg, https://luckly007.oss-cn-beijing.aliyuncs.com/img/15814253180199.jpg 1.5x, https://luckly007.oss-cn-beijing.aliyuncs.com/img/15814253180199.jpg 2x"
        data-sizes="auto"
        alt="https://luckly007.oss-cn-beijing.aliyuncs.com/img/15814253180199.jpg"
        title="-w850" /></p>
<p>如果后续有 <code>Image</code> 显示同一个 key 对应的图片，再次调用 <code>ImageProvider.resolve()</code> 的速度将大为提升(网络图片会尤其明显)，当然前提是该图片仍然在图片缓存中。</p>
<h1 id="参考">参考</h1>
<ul>
<li><a href="https://api.flutter.dev/flutter/widgets/precacheImage.html" target="_blank" rel="noopener noreffer">precacheImage function - widgets library - Dart API</a></li>
</ul></div><div class="post-footer" id="post-footer">
    <div class="post-info">
        <div class="post-info-line">
            <div class="post-info-mod">
                <span>Updated on 2021-07-31</span>
            </div>
            <div class="post-info-license"></div>
        </div>
        <div class="post-info-line">
            <div class="post-info-md"></div>
            <div class="post-info-share">
                <span></span>
            </div>
        </div>
    </div>

    <div class="post-info-more">
        <section class="post-tags"><i class="fas fa-tags fa-fw"></i>&nbsp;<a href="/tags/flutter/">flutter</a></section>
        <section>
            <span><a href="javascript:void(0);" onclick="window.history.back();">Back</a></span>&nbsp;|&nbsp;<span><a href="/">Home</a></span>
        </section>
    </div>

    <div class="post-nav"><a href="/posts/flutter/Flutter-drawFrame/" class="prev" rel="prev" title="《Flutter DrawFrame（）》"><i class="fas fa-angle-left fa-fw"></i>《Flutter DrawFrame（）》</a>
            <a href="/posts/flutter/Flutter%E4%B9%8BStack%E7%94%A8%E6%B3%95%E5%B0%8F%E7%BB%93/" class="next" rel="next" title="《Flutter之Stack用法小结》">《Flutter之Stack用法小结》<i class="fas fa-angle-right fa-fw"></i></a></div>
</div>
</article></div>
            </main><footer class="footer">
        <div class="footer-container"><div class="footer-line">Powered by <a href="https://gohugo.io/" target="_blank" rel="noopener noreffer" title="Hugo 0.79.1">Hugo</a> | Theme - <a href="https://github.com/dillonzq/LoveIt" target="_blank" rel="noopener noreffer" title="LoveIt 0.2.10"><i class="far fa-kiss-wink-heart fa-fw"></i> LoveIt</a>
                </div><div class="footer-line"><i class="far fa-copyright fa-fw"></i><span itemprop="copyrightYear">2021</span><span class="author" itemprop="copyrightHolder">&nbsp;<a href="/" target="_blank">飞雪无情</a></span></div>
        </div>
    </footer></div>

        <div id="fixed-buttons"><a href="#" id="back-to-top" class="fixed-button" title="Back to Top">
                <i class="fas fa-arrow-up fa-fw"></i>
            </a><a href="#" id="view-comments" class="fixed-button" title="View Comments">
                <i class="fas fa-comment fa-fw"></i>
            </a>
        </div><script type="text/javascript" src="/lib/smooth-scroll/smooth-scroll.min.js"></script><script type="text/javascript" src="/lib/lazysizes/lazysizes.min.js"></script><script type="text/javascript" src="/lib/clipboard/clipboard.min.js"></script><script type="text/javascript" src="/lib/sharer/sharer.min.js"></script><script type="text/javascript">window.config={"code":{"copyTitle":"Copy to clipboard","maxShownLines":10},"comment":{}};</script><script type="text/javascript" src="/js/theme.min.js"></script></body>
</html>
